package afcp.alumni.dao.base;

import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import javax.jdo.Extent;
import javax.jdo.JDOHelper;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Query;
import javax.jdo.annotations.Transactional;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import afcp.alumni.model.SearchResult;
import afcp.alumni.model.base.DatastoreEntity;
import afcp.alumni.model.base.UniqueField;
import afcp.alumni.model.base.WithUniqueField;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

// TODO make a "add" method that checks in a Transaction if the entity exists.

public abstract class DaoDatastoreJDO<T extends DatastoreEntity> extends DaoDatastoreCommon<T> {
	
	private static final Log logger = LogFactory.getLog(DaoDatastoreJDO.class);
	
	protected static final PersistenceManagerFactory pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional");
	protected static PersistenceManager persistenceManager = pmfInstance.getPersistenceManager();
	
	@Override
	public List<T> findAll() {
		logger.info("Searching for entities " + this.className + "...");
		List<T> entities = new LinkedList<>();
		Extent<T> extent = persistenceManager.getExtent(classObject, false);
		for (T entity : extent) {
		  entities.add(entity);
		}
		extent.closeAll();
		return entities;
	}

	/**
	 * Indexes are 0-based.
	 */
	@Override
	public SearchResult<T> findOnePage(int page, int perPage, Locale locale) {
		SearchResult<T> entities = new SearchResult<>(page, perPage, locale);
		entities.setTotalResults(this.getTotalResults());
		if (entities.hasResults()) {
			int firstIndexIncluded = (page-1)*perPage;
			int lastIndexExcluded = page*perPage;
			logger.info("Searching for entities " + this.className + " from element " + (firstIndexIncluded+1) + " to element " + lastIndexExcluded + "...");
			Query q = persistenceManager.newQuery(classObject);
			q.setRange(firstIndexIncluded, lastIndexExcluded);
			@SuppressWarnings("unchecked")
			List<T> executeResult = (List<T>) q.execute();
			entities.getList().addAll(executeResult);
//			SearchResult<T> entities = new SearchResult<>(executeResult);
		}
		return entities;
	}
	
	private long getTotalResults() {
		logger.info("Counting the total number of entities " + this.className + ".");
		Query q = persistenceManager.newQuery(classObject);
		q.setResult("count(this)");
		long count;
		try {
			count = (long) q.execute();
		} catch (Exception e) {
			logger.warn("Could not count entities, an error occurred...", e);
			count = -1;
		}
		return count;
	}
	
	@Override
	public T find(String id) {
		logger.info("Searching for entity " + this.className + " with id: " + id + "...");
		Key k = KeyFactory.createKey(this.getAncestorKey(), className, id);
		return find(k);
	}

	@Override
	public T findFromKeyString(String key) {
		Key k = KeyFactory.stringToKey(key);
		return find(k);
	}
	
	private T find(Key key) {
		try {
			return persistenceManager.getObjectById(classObject, key);
		} catch (JDOObjectNotFoundException e) {
			logger.info("Entity " + className + " with id: " + KeyFactory.keyToString(key) + " not found.");
			return null;
		}
	}
	
	/**
	 * @param field
	 *            : should be bound to the entity corresponding to this DAO. If
	 *            not, returns <code>null</code>.
	 */
	@Override
	@Transactional
	public <U extends WithUniqueField> U find(UniqueField<U> field) {
		
		// TODO subclass instead of throwing exception.
		
		// A different generic type is used in order to avoid subclassing this Dao.
		// There are already too many subclasses.
		if (!classObject.equals(field.entityClass())) {
			logger.error("field of class "
					+ field.getClass() + " is bound to "
					+ field.getEntity().getClass()
					+ ", but it sould be bound to " + classObject);
			return null;
		}
		logger.info("Searching for entity " + this.className + " with unique field: " + field.getId() + "...");
		Key k = field.getKey();
		try {
			// It's weird but field.getClass() returns Class<? extends UniqueField>
			// instead of Class<? extends UniqueField<U>>, so there is a warning.
			// field being an instance of UniqueField<U>, we know which object is returned.
			@SuppressWarnings({ "unchecked" })
			UniqueField<U> foundUniqueField = persistenceManager.getObjectById(field.getClass(), k);
			return foundUniqueField.getEntity();
		} catch (JDOObjectNotFoundException e) {
			logger.info("Entity " + className + " with id: " + field.getId() + " not found.");
			return null;
		}
	}
	
	@Override
	public SearchResult<T> findFromKeyStrings(List<String> ids, int page, int perPage, Locale locale) {
		
//		List<Key> keys = new LinkedList<>();
//		for (String id : ids) {
//			keys.add(KeyFactory.createKey(className, id));
//		}
//		return persistenceManager.getObjectsById(keys);

		logger.info("Searching for entities " + this.className + " with ids: " + ids + "...");
		SearchResult<T> entities = new SearchResult<>(page, perPage, locale);
		entities.setTotalResults(ids.size());
		if (entities.hasResults()) {
			// TODO this implementation is not optimized. May be replaced by JDO methods.
			for (String id : ids) {
				entities.getList().add(findFromKeyString(id));
			}
		}
		return entities;
	}
	
	@Override
	public boolean exists(T entity) {
		if (entity == null) {
			return false;
		}
		if (entity instanceof WithUniqueField && entity.getKey() == null) {
			// TODO subclass instead of testing interface
			return find(((WithUniqueField) entity).getUniqueField()) != null;
		} else {

			setKey(entity);
			logger.info("checking if entity " + className + " with id: " + entity.getId() + " exists in data store...");
			String id = KeyFactory.keyToString(entity.getKey());
			T entityFound;
			try {
				entityFound = persistenceManager.getObjectById(classObject, id);
			} catch (JDOObjectNotFoundException e) {
				entityFound = null;
			}
			boolean exists = entityFound != null;
			
			
//			Query q = persistenceManager.newQuery(classObject, "key == keyParam");
//			q.declareParameters(Key.class.getName() + " keyParam");
////			q.execute(entity.getKey());
//		    @SuppressWarnings("unchecked")
////			List<T> entities = (List<T>) q.execute(entity.getTheKey());
//			List<T> entities = (List<T>) q.execute(id);
//		    T entityFound = null;
//		    boolean exists = entities != null && !entities.isEmpty() && (entityFound = entities.get(0)) != null;
		    
		    if (exists) {
				logger.info("entity " + className + " with id: " + entityFound.getId() + " and key: " + entityFound.getKey() + " already exists.");
		    } else {
		    	logger.info("entity " + className + " with id: " + entity.getId() + " not found.");
		    }
			return exists;
			
		}
	}
	@Override
	public boolean addOrUpdate(T entity) {
		if (entity == null) {
			logger.error("Can't add null entity " + className + ".");
			return false;
		}
		if (logger.isInfoEnabled()) {
			String id = entity.getId();
			if (id != null) {
				logger.info("Add entity " + className + " with id: " + entity.getId() + ".");
			} else {
				logger.info("Add entity " + className + " " + entity + ".");
			}
		}
		setKey(entity);
		persistenceManager.makePersistent(entity);
		return true;
	}
	@Override
	public boolean addOrUpdate(List<T> entities) {
		if (entities == null || entities.contains(null)) {
			logger.error("Can't add null entity " + className + ".");
			return false;
		}
		if (logger.isInfoEnabled()) {
			List<String> ids = new LinkedList<>();
			for (T entity : entities) {
				ids.add(entity.getId());
			}
			logger.info("Add entities " + className + " with ids: " + ids + ".");
		}
		if (!entities.isEmpty()) {
			setKey(entities);
			persistenceManager.makePersistentAll(entities);
		}
		return true;
	}
	private void remove(T entity) {
		if (entity instanceof WithUniqueField) {
			UniqueField<? extends WithUniqueField> uniqueField = ((WithUniqueField) entity).getUniqueField();
			persistenceManager.deletePersistent(uniqueField);
		}
		persistenceManager.deletePersistent(entity);
	}
	@Override
	public T removeFromKeyString(String id) {
		logger.info("Remove entity " + this.className + " with id: " + id + "." + ".");
		Key k = KeyFactory.stringToKey(id);
		T entity = find(k);
		T detachedCopy = persistenceManager.detachCopy(entity);
		remove(entity);
//		persistenceManager.deletePersistent(entity);
//		persistenceManager.flush();
		return detachedCopy;
	}
	@Override
	public T remove(String id) {
		logger.info("Remove entity " + this.className + " with id: " + id + "." + ".");
		T entity = find(id);
		T detachedCopy = persistenceManager.detachCopy(entity);
		remove(entity);
//		persistenceManager.deletePersistent(entity);
		return detachedCopy;
	}
	@Override
	public boolean remove(List<String> ids) {
		logger.info("Remove entities " + this.className + " with ids: " + ids + ".");
		// Pas sûr que ça marche... il ne faut pas plutôt une liste d'entités ?
		persistenceManager.deletePersistentAll(ids);
		return true;
	}
	@Override
	public boolean removeAll() {
		logger.info("Remove all entities " + this.className + ".");
		Query q = persistenceManager.newQuery(classObject);
		q.deletePersistentAll();
		
		return true;
	}
	@Override
	public boolean update(T entity) {
		if (entity == null) {
			logger.error("Can't add null entity " + className + ".");
			return false;
		}
		Key key = entity.getKey();
		if (key == null) {
			logger.error("Can't update entity of type " + className
					+ " if the object has no key.");
			return false;
		}
		// TODO subclass instead of testing interface
		if (entity instanceof WithUniqueField) {
			UniqueField<? extends WithUniqueField> oldEntityUniqueField = ((WithUniqueField) find(key))
					.getUniqueField();
			Key oldUniqueFieldKey = oldEntityUniqueField.getKey();
			Key newUniqueFieldKey = ((WithUniqueField) entity).getUniqueField().getKey();
			if (!newUniqueFieldKey.equals(oldUniqueFieldKey)) {
				persistenceManager.deletePersistent(oldEntityUniqueField);
			}
		}
		
		logger.info("Update entity " + className + " with id: " + entity.getId() + ".");
		persistenceManager.makePersistent(entity);
		return true;
	}

}
